﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.XPath;
using Microsoft.Win32;

namespace Microsoft.WindowsAzure.Accelerator.Synchronization
{
    /// <summary>
    /// Content type for storing file extension to mime type relationships.
    /// </summary>
    public class ContentType
    {
        private SortedDictionary<String, MimeType> _mimeTypes;
     
        /// <summary>
        /// Gets or sets the file extension.
        /// </summary>
        public String FileExtension { get; set; }
        
        /// <summary>
        /// Gets or sets the MIME types.
        /// </summary>
        public SortedDictionary<String, MimeType> Types 
        { 
            get { return _mimeTypes ?? (_mimeTypes = new SortedDictionary<String, MimeType>()); }
            set { _mimeTypes = value; }
        }
    }

    /// <summary>
    /// Mime type.
    /// </summary>
    public class MimeType
    {
        /// <summary>
        /// Gets or sets a value indicating whether this MIME type is allowed.
        /// </summary>
        public Boolean IsAllowed { get; set; }
        
        /// <summary>
        /// Gets or sets the MIME types origin information.
        /// </summary>
        public MimeSource MimeSource { get; set; }

        /// <summary>
        /// Gets or sets the type.
        /// </summary>
        public String Type { get; set; }
    }

    /// <summary>
    /// Mime type origin information.
    /// </summary>
    [Flags]
    public enum MimeSource
    {

        /// <summary>
        /// Mime content loaded from Xml file.
        /// </summary>
        XML = 1,
        /// <summary>
        /// Mime content types loaded from registry.
        /// </summary>
        Registry = 2,
        /// <summary>
        /// Mime content type loaded from application host file.
        /// </summary>
        IIS = 4,
        /// <summary>
        /// User suplied or default content.
        /// </summary>
        Unknown = 8
    }
    
    /// <summary>
    /// Based on public domain file extension and mime type information. This class is neither connected 
    /// or dependant upon IIS metadata.
    /// </summary>
    /// <remarks>
    /// The 'isAllowed' field of the XML is implemented as 'isEnabled' due to time constrains.  Need to
    /// update this as time permits.
    /// </remarks>
    public sealed class MimeTypes : IXmlSerializable
    {
        private const String XmlFile = @"Synchronization\MimeTypes.xml";
        private String _defaultMimeType = @"application/octet-stream";
        private static volatile MimeTypes _instance;
        private static Object _syncRoot = new Object();
        private SortedDictionary<String, ContentType> _types;
        
        /// <summary>
        /// Gets the single instance of this class.
        /// </summary>
        public static MimeTypes Instance { get { if (_instance == null){ lock (_syncRoot){ return _instance ?? ( _instance = new MimeTypes() ); } } return _instance; } }

        /// <summary>
        /// Gets or sets the default mime content type for unknown file extensions.
        /// </summary>
        public ContentType DefaultContentType { get; set; }

        /// <summary>
        /// Sorted list of mime content types.
        /// </summary>
        private SortedDictionary<String, ContentType> ContentTypes { get { return _types ?? (_types = new SortedDictionary<String, ContentType>()); } }

        /// <summary>
        /// Initializes a new instance of the <see cref="MimeTypes"/> class.
        /// </summary>
        private MimeTypes()
        {
            try { 
                LoadFromRegistry();
                Trace.TraceInformation("Successfully loaded MIME types from Registry.");
            } catch {
                Trace.TraceError("Unable to load MIME types from registry.");
            }
            
            try { 
                LoadFromIIS();
                Trace.TraceInformation("Successfully loaded MIME types from IIS.");
            } catch {
                Trace.TraceError("Unable to load MIME types from IIS.");
            }
            
            try { 
                LoadFromXml();
                Trace.TraceInformation("Successfully loaded MIME types from XML.");
            } catch {
                Trace.TraceError("Unable to load MIME types from '{0}'.", XmlFile);
            }
            
            DefaultContentType = new ContentType
                                     {
                                         FileExtension = "",
                                         Types = new SortedDictionary<String, MimeType>
                                                        {
                                                            { _defaultMimeType, new MimeType
                                                                                    {
                                                                                        IsAllowed = true,
                                                                                        Type = _defaultMimeType,
                                                                                        MimeSource = MimeSource.Unknown
                                                                                    }}
                                                        }
                                     };
        }

        /// <summary>
        /// Gets or sets the content mime type associated with the specified file extension.
        /// </summary>
        /// <value>Content type.</value>
        public String this[String fileExtension]
        {
            get { return GetMimeType(fileExtension).Type; }
        }

        /// <summary>
        /// Gets the content type for the file extension; return the default content type if file extension not found.
        /// </summary>
        /// <param name="fileExtension">The file extension.</param>
        /// <returns>Content type.</returns>
        private MimeType GetMimeType(String fileExtension)
        {
            String lExt = fileExtension.ToLower().EnsureStartsWith(".");
            return ContentTypes.ContainsKey(lExt)
                       ? ContentTypes[lExt].Types.Values.FirstOrDefault()
                       : DefaultContentType.Types.Values.FirstOrDefault();
        }

        /// <summary>
        /// Loads from mime types from an XML file.
        /// </summary>
        private void LoadFromXml()
        {
            if (File.Exists(XmlFile))
            {
                XDocument xdoc = XDocument.Load(XmlFile);
                foreach (var e in xdoc.XPathSelectElements("//MimeType").ToList())
                {
                    AddType(e.GetAttribute("fileExtension"), e.GetAttribute("type"), MimeSource.XML, Boolean.Parse(e.GetAttribute("isAllowed")));
                }
            }
        }

        /// <summary>
        /// Load mime types from the application hosts configuration.
        /// </summary>
        private void LoadFromIIS()
        {
            String xApplicationHost = Environment.ExpandEnvironmentVariables(@"%windir%\system32\inetsrv\config\applicationHost.config");
            if (File.Exists(xApplicationHost))
            {
                XDocument xdoc = XDocument.Load(xApplicationHost);
                foreach (var e in xdoc.XPathSelectElements("//staticContent/mimeMap").ToList())
                {
                    AddType(e.GetAttribute("fileExtension"), e.GetAttribute("mimeType"), MimeSource.IIS, true);
                }
            }            
        }

        /// <summary>
        /// Gets the registry MIME types.
        /// </summary>
        private void LoadFromRegistry()
        {
            using (RegistryKey contentTypes = Registry.ClassesRoot.OpenSubKey("MIME\\Database\\Content Type"))
            {
                if (contentTypes != null)
                {
                    foreach (String name in contentTypes.GetSubKeyNames()) 
                    {
                        RegistryKey contentKey = contentTypes.OpenSubKey(name);
                        if (contentKey != null)
                        {
                            String extension = contentKey.GetValue("Extension") as String;
                            if (extension != null)
                            {
                                AddType(extension, name, MimeSource.Registry, true);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Adds the type.
        /// </summary>
        /// <param name="fileExtension">The file extension.</param>
        /// <param name="mimeType">Type of the MIME.</param>
        /// <param name="mimeSource">The MIME source.</param>
        /// <param name="isAllowed">if set to <c>true</c> [is allowed].</param>
        private void AddType(String fileExtension, String mimeType, MimeSource mimeSource, Boolean isAllowed)
        {
            String lType = mimeType.ToLower();
            String lExt = fileExtension.ToLower().EnsureStartsWith(".");
            ContentType ct = ContentTypes.ContainsKey(lExt)
                                 ? ContentTypes[lExt]
                                 : new ContentType
                                       {
                                           FileExtension = fileExtension
                                       };

            MimeType mt = ct.Types.ContainsKey(lType)
                              ? ct.Types[lType]
                              : new MimeType
                                    {
                                        Type = mimeType
                                    };
            mt.MimeSource = mt.MimeSource | mimeSource;
            mt.IsAllowed = mt.IsAllowed | isAllowed;
            ct.Types[lType] = mt;
            ContentTypes[lExt] = ct;
        }

        /// <summary>
        /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> to the class.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema"/> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/> method.
        /// </returns>
        public XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> stream from which the object is deserialized.</param>
        public void ReadXml(XmlReader reader)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter"/> stream to which the object is serialized.</param>
        public void WriteXml(XmlWriter writer)
        {
            Func<MimeSource, String> source = (MimeSource ms) => (from MimeSource v in Enum.GetValues(typeof (MimeSource))
                                                                  where (ms & v) == v
                                                                  select v).Aggregate(String.Empty, (current, v) => current + Enum.GetName(typeof (MimeSource), v) + ',').Trim(',');

            var e = new XElement("ContentTypes", ContentTypes.Select(ct => 
                        new XElement("ContentType", 
                            new XAttribute("fileExtension", ct.Value.FileExtension), ct.Value.Types.Select(mt => 
                            new XElement("mimeType",
                                new XAttribute("type", mt.Value.Type),
                                new XAttribute("source", source(mt.Value.MimeSource)),
                                new XAttribute("isAllowed", mt.Value.IsAllowed))))));
            writer.WriteNode(e.CreateReader(), true);
        }
    }
}
